package com.park.tool;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@Slf4j
public class Application {
    private static String datePattern = "yyyy-MM-dd HH:mm:ss";
    private static Object exitLock = new Object();

    @Value("${spring.mail.username}")
    private String notificationName;
    @Value("${park.api.carinfo}")
    private String carInfoUrl;
    @Value("${park.api.userinfo}")
    private String userinfoUrl;
    @Value("${park.api.couponinfo}")
    private String couponUrl;
    @Value("${park.api.pay}")
    private String payUrl;
    @Value("${park.car}")
    private String carName;
    @Value("${park.mail}")
    private String carMail;
    @Value("${park.openids}")
    private String[] openIds;

    @Autowired
    private JavaMailSenderImpl mailNotification;

    private ExecutorService executorService;

    private OkHttpClient httpClient = new OkHttpClient().newBuilder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(60, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .build();
    private Map<String, Boolean> openIdUsed = new HashMap<>();
    private Map<String, LocalDateTime> carNextScheduleTime = new HashMap<>();

    public static void main(String[] args) throws IOException, InterruptedException {
        SpringApplication.run(Application.class, args);
        log.info("Application started");
        while (true) {
            synchronized (exitLock) {
                log.info("start ok");
                exitLock.wait();
            }
        }
    }

    @PostConstruct
    protected void run() throws InterruptedException, IOException {
        executorService = Executors.newScheduledThreadPool(2);
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
            try {
                processParkFees();
            } catch (Exception e) {
                sendError(null, e);
                log.error("", e);
            }
        }, 0L, 60L, TimeUnit.SECONDS);
    }

    private void processParkFees() {
        if (!resetDatas()) {
            log.info("not in schedule time=[8~22]");
            return;
        }
        LocalDateTime now = LocalDateTime.now();
        log.info("---------------------------------------");

        if (carNextScheduleTime.get(carName) != null && now.isBefore(carNextScheduleTime.get(carName))) {
            log.info("{} next schedule time={}", carName, formatTimestamp(carNextScheduleTime.get(carName)));
            return;
        }
        CarInfo carInfo = getCarInfo(carName);
        if (carInfo == null) {
            log.info("{} not in park", carName);
            carNextScheduleTime.put(carName, now.plusMinutes(30));
            return;
        } else {
            LocalDateTime next = now.plusMinutes(20);
            carNextScheduleTime.put(carName, next);
            if (carInfo.getChargeMoney() < 800) {
                log.info("{} money={} RMB, skip", carName, carInfo.getChargeMoney() / 100.0);
                log.info("{} next schedule time={}", carName, formatTimestamp(carNextScheduleTime.get(carName)));
                return;
            } else if (carInfo.getChargeMoney() > 800) {
                sendError(carName, String.format("%s money=%s RMB, 需要人工介入", carName, carInfo.getChargeMoney() / 100.0));
                log.warn("{} money={} RMB, 需要人工介入", carName, carInfo.getChargeMoney() / 100.0);
                return;
            }
        }
        // pay flow 
        payFlow:
        for (int i = 0; i < openIds.length; i++) {
            String openId = openIds[i];
            if (openIdUsed.get(openId) != null && openIdUsed.get(openId)) {//已经用过
                log.info("{} openid {} is used, retry", carName, openId);
                if (i == openIds.length - 1) {
                    carNextScheduleTime.put(carName, now.plusDays(1).withMinuteOfHour(0).withHourOfDay(8));
                    log.info("{} all openid used, schedule nex day {}", carName, formatTimestamp(carNextScheduleTime.get(carName)));
                }
                continue;
            }

            User user = getUser(openId);
            Coupon availableCoupon = getAvailableCoupon(user.getId());
            if (availableCoupon == null) {
                log.info("{} openid {} has no coupon", carName, openId);
                if (i == openIds.length - 1) {
                    log.info("{} all openid used, schedule nex day {}", carName, formatTimestamp(carNextScheduleTime.get(carName)));
                    if (now.getHourOfDay() >= 9 && now.getHourOfDay() <= 12) {
                        sendMail(carName + "优惠券异常通知", "优惠券提前用完，商场可能没有正常发券");
                    }
                }
                continue;
            }
            Res pay = pay(carName, user, availableCoupon);
            if (!pay.isSuccess()) {//不成功
                log.error("{} paid by openid {} error {}", carName, openId, pay.getResMsg());
                sendError(carName, String.format("%s 支付未成功，请关注", carName));
                continue payFlow;
            } else {//成功
                sendMail(carName + "缴费通知", String.format("%s支付成功%s元", formatTimestamp(LocalDateTime.now()), carInfo.getChargeMoney() / 100.0));
                openIdUsed.put(openId, true);
                if (i == openIds.length - 1) {
                    carNextScheduleTime.put(carName, now.plusDays(1).withMinuteOfHour(0).withHourOfDay(8));
                    log.info("{} all openid used, schedule nex day {}", carName, formatTimestamp(carNextScheduleTime.get(carName)));
                }
                break payFlow;
            }

        }
    }

    private boolean resetDatas() {//08:01~08:10重置使用数据
        int hour = LocalTime.now().getHourOfDay();
        if (hour == 8 && LocalTime.now().getMinuteOfHour() <= 10) {//10分钟时间清理数据
            openIdUsed.clear();
        }
        if (hour >= 8 && hour <= 22) {
            return true;
        }
        LocalDateTime now = LocalDateTime.now();
        now.withHourOfDay(8).withMinuteOfHour(0);
        if (hour <= 8 && hour >= 0) {
        } else if (hour >= 22 && hour <= 23) {
            now.plusDays(1);
        }
        carNextScheduleTime.put(carName, now);
        return false;
    }

    private Res pay(String car, User user, Coupon availableCoupon) {
        log.info("{} in paying, using openid {}, coupon id {}", car, user.getOpenid(), availableCoupon.getCodeId());
        String payRes = httpPost(payUrl, new ParamBuilder()
                .addParam("carNumber", car)
                .addParam("memberId", user.getId())
                .addParam("sellerName", "weixin")
                .addParam("payType", "10")
                .addParam("openid", user.getOpenid())
                .addParam("codeIdStr", "" + availableCoupon.getCodeId())
                .build()
        );
        Res pay = JSON.parseObject(payRes, Res.class);
        log.info("{} paid, {}", car, pay);
        return pay;
    }

    private Coupon getAvailableCoupon(int userId) {
        String res = httpPost(couponUrl,
                new ParamBuilder().addParam("mId", userId)
                        .addParam("page", 1)
                        .addParam("size", 20)
                        .addParam("state", 0)
                        .addParam("type", 2).build()
        );
        List<Coupon> coupons = parseList(res, Coupon.class);
        if (coupons == null || coupons.size() == 0) {
            return null;
        } else {
            Collections.sort(coupons, (o1, o2) -> o1.getUseEnd().compareTo(o2.getUseEnd()) >= 0 ? -1 : 1);
            return coupons.get(0);
        }
    }

    private User getUser(String openId) {
        String userStr = httpPost(userinfoUrl, new ParamBuilder().addParam("openid", openId).build());
        User user = parseObject(userStr, User.class);
        return user;
    }

    private CarInfo getCarInfo(String car) {
        String res = httpPost(carInfoUrl, new ParamBuilder().addParam("carNumber", car).build());
        CarInfo carInfo = parseObject(res, CarInfo.class);
        if (carInfo != null) {
            log.info("{} inTime={}, charge={} cent", car, carInfo.getInTime(), carInfo.getChargeMoney());
        }
        return carInfo;
    }


    static <T> T parseObject(String str, Class<T> tClass) {
        Res<T> object = JSON.parseObject(str, new TypeReference<Res<T>>(tClass) {
        });
        return (T) object.getData();
    }

    static <T> List<T> parseList(String str, Class<T> tClass) {
        ListRes<T> list = JSON.parseObject(str, new TypeReference<ListRes<T>>(tClass) {
        });
        return list.getData();
    }


    private String httpPost(String url, Object data) {
        MediaType mediaType = MediaType.parse("application/json; charset=UTF-8");
        String bodyStr = JSON.toJSONString(data);
        RequestBody requestBody = RequestBody.create(mediaType, bodyStr);
        Request request = new Request.Builder()
                .url(url)
                .method("POST", requestBody)
                .addHeader("Content-Type", "application/json; charset=UTF-8")
                .addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) MicroMessenger/6.8.0(0x16080000) MacWechat/2.4.2(0x12040212) NetType/WIFI WindowsWechat")
                .build();
        try {
            Response response = httpClient.newCall(request).execute();
            String res = response.body().string();
            Res jsonRes = JSON.parseObject(res, Res.class);
            if (jsonRes != null && !jsonRes.isSuccess()
                    && jsonRes.getResCode() != 10007 //这是两个错误码
                    && jsonRes.getResCode() != 10099) {
                sendError(null, String.format("request %s resCode=%s res=%s", url, jsonRes.getResCode(), jsonRes.getResMsg()));
            }
            return res;
        } catch (IOException e) {
            log.error("http post error.", e);
            sendError(null, String.format("request %s error %s", url, e.toString()));
        }
        return "{}";
    }

    static String formatTimestamp(LocalDateTime time) {
        return time.toString(datePattern);
    }

    static class ParamBuilder {
        Map<String, Object> map = new LinkedHashMap<>();

        public ParamBuilder() {
        }

        ParamBuilder addParam(String key, Object value) {
            map.put(key, value);
            return this;
        }

        Map<String, Object> build() {
            Map<String, Object> data = new HashMap<>();
            data.put("userCode", "qqkj");
            data.put("passwd", "123456");
            data.put("data", DesUtil.encrypt(JSON.toJSONString(this.map)));
            return data;
        }
    }

    private void sendError(String carName, Throwable e) {
        if (carName == null) {
            sendMail("park缴费Error", e.toString());
        } else {
            sendMail(carName + "缴费Error", e.toString());
        }
    }

    private void sendError(String car, String content) {
        sendMail(car + "缴费error", content);
    }

    private void sendMail(String title, String content) {
        executorService.execute(() -> {
            if (StringUtils.isNoneBlank(notificationName) && StringUtils.isNoneBlank(carMail)) {
                final String email = carMail;
                SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
                simpleMailMessage.setTo(email);
                simpleMailMessage.setFrom(notificationName);
                simpleMailMessage.setSubject(title);
                simpleMailMessage.setText(content);
                try {
                    mailNotification.send(simpleMailMessage);
                } catch (MailException e) {
                    log.error("mail error", e);
                }
            }

        });

    }
}
